﻿using System;
using System.Collections.Generic;
using System.Text;
using llAma.Base;
using dAmn;
using llAma.Backend.Interface;
using MySql.Data.MySqlClient;
using System.Diagnostics;
using llamaServer;
using llAma;
using System.Threading;

//Plaguethenet's notes:
//javascript:alert(dAmn_Client_Server='localhost');dAmn_Logoff()

namespace socialdeveloperBackend
{
    public class PrivclassInfo
    {
        int mId;
        int order;
        string name;
        List<int> members = new List<int>();
        public int Order
        {
            get { return order; }
        }
        public string Name
        {
            get { return name; }
        }
        public int Id
        {
            get { return mId; }
        }
        public bool hasMember(int memberId)
        {
            return members.Contains(memberId);
        }
        public IUserData[] GetAllMembers()
        {
            List<IUserData> ret = new List<IUserData>();
            for (int x = 0; x < members.Count; x++)
            {
                ret.Add(GlobalUserCache.GetUser(members[x]));
            }
            return ret.ToArray();
        }
        public void Refresh()
        {
            using (MySqlConnection conn = SDBackend.CreateConnection())
            {
                using (MySqlDataReader dr = SDBackend.ExecuteReader(conn, "SELECT * FROM `tbl_privclass` WHERE `id`=@pcid", new MySqlParameter("@pcid", this.mId)))
                {
                    if (dr.Read())
                    {
                        //ID will never change, do not update it, its useless.
                        PrivclassInfo pc = new PrivclassInfo(dr);
                        this.name = pc.name;
                        this.members = pc.members;
                        this.order = pc.order;
                    }
                }
            }
            //And since everything was on the stack, the GC will dump them all once this function exits, YAY for free memory.
        }
        public PrivclassInfo(MySqlDataReader dr)
        {
            mId = (int)dr["id"];
            order = (int)dr["order"];
            name = (string)dr["name"];
            using (MySqlConnection conn = SDBackend.CreateConnection())
            {
                using (MySqlDataReader mReader =
                    SDBackend.ExecuteReader(conn, "SELECT * FROM tbl_privclass_members WHERE `id` = @id", new MySqlParameter("@id", mId)))
                {
                    while (mReader.Read())
                        members.Add((int)mReader["memberid"]);
                }
            }
        }
    }
    public class DBPropertyManager : PropertyManagerBase
    {
        ChatChannel mChannel;
        public DBPropertyManager(ChatChannel channel)
            : this()
        {
            mChannel = channel;
            this.Namespace = mChannel.Namespace;
            LoadPCs();
        }
        private class TextPropertyBlock
        {
            string mPropertyName = string.Empty;
            string mData = string.Empty;
            int mUser = -1;
            long mTs = 0;
            int mId = -1;
            //We will awlays get a reader with 1 row.
            //Read will have already been called on it too.
            public TextPropertyBlock(MySqlDataReader reader)
            {
                if (reader.Read())
                {
                    mPropertyName = (string)reader["propertyname"];
                    mTs = (long)reader["when"];
                    mUser = (int)reader["setby"];
                    mData = (string)reader["value"];
                    mId = (int)reader["id"];
                    reader.Close();
                }
            }

            public string Name
            {
                get { return mPropertyName; }
                set { mPropertyName = value; }
            }
            public string Value
            {
                get { return mData; }
                set { mData = value; }
            }
            public string Username
            {
                get
                {
                    IUserData ui = GlobalUserCache.GetUser(mUser);
                    if (ui == null)
                        return string.Empty;
                    return ui.getInfo("username");
                }
                set
                {
                    IUserData ui = GlobalUserCache.GetUser(value);
                    if (ui == null)
                        throw new InvalidOperationException("Failed to look up that user, bug?");
                    mUser = int.Parse(ui.getInfo("id"));
                }
            }
            public long Timestamp
            {
                get { return mTs; }
                set { mTs = value; }
            }
            public int ID
            {
                get { return mId; }
                set { mId = value; }
            }
            public static TextPropertyBlock LookupProperty(int id, string name)
            {
                using (MySqlConnection conn = SDBackend.CreateConnection())
                {
                    return new TextPropertyBlock(
                    SDBackend.ExecuteReader(conn,
                    "SELECT * FROM tbl_channel_properties WHERE channel_id = ?cid AND propertyname = ?pname",
                    new MySqlParameter("?cid", id), new MySqlParameter("?pname", name)));
                }
            }
        }
        //Dictionary<string, CacheEntry<TextPropertyBlock>> propCache = new Dictionary<string, CacheEntry<TextPropertyBlock>>();
        //ServerCache<string, TextPropertyBlock> propCache = new ServerCache<string, TextPropertyBlock>();
        private Packet GetPropertyPacket(string propName)
        {
            TextPropertyBlock tmp;
            Packet outgoing = mChannel.EmptyRecvPacket;
            outgoing.cmd = "property";
            tmp = TextPropertyBlock.LookupProperty(mChannel.ID, propName);
            outgoing.args["p"] = tmp.Name != string.Empty ? tmp.Name : propName;
            outgoing.args["by"] = tmp.Username;
            outgoing.args["ts"] = tmp.Timestamp.ToString();
            outgoing.body = tmp.Value;
            return outgoing;
        }
        private void UpdateProperty(int room, int userid, string pname, string val)
        {
            string CmdStr = "UPDATE `tbl_channel_properties` SET `setby`=@uid, `when`=@when, `value`=@value WHERE `channel_id`=@cid AND `propertyname`=@propname";
            using (MySqlConnection conn = SDBackend.CreateConnection())
            {
                using (MySqlCommand cmd = new MySqlCommand())
                {
                    cmd.Connection = conn;
                    cmd.Parameters.AddWithValue("@uid", userid);
                    cmd.Parameters.AddWithValue("@when", GetTS(DateTime.Now));
                    cmd.Parameters.AddWithValue("@value", val);
                    cmd.Parameters.AddWithValue("@cid", room);
                    cmd.Parameters.AddWithValue("@propname", pname);
                    cmd.CommandText = CmdStr;
                    cmd.ExecuteNonQuery();
                }
            }
        }
        private void CreateProperty(int room, int userid, string pname, string val)
        {
            string CmdStr = "INSERT INTO tbl_channel_properties (`channel_id`, `propertyname`, `setby`, `when`, `value`) VALUES (@cid, @propname, @uid, @when, @value)";
            using (MySqlConnection conn = SDBackend.CreateConnection())
            {
                using (MySqlCommand cmd = new MySqlCommand())
                {
                    cmd.Connection = conn;
                    cmd.Parameters.AddWithValue("@uid", userid);
                    cmd.Parameters.AddWithValue("@when", GetTS(DateTime.Now));
                    cmd.Parameters.AddWithValue("@value", val);
                    cmd.Parameters.AddWithValue("@cid", room);
                    cmd.Parameters.AddWithValue("@propname", pname);
                    cmd.CommandText = CmdStr;
                    cmd.ExecuteNonQuery();
                }
            }
        }
        private void SetProperty(IConnection conn, string propName, string newValue)
        {
            TextPropertyBlock tpb = TextPropertyBlock.LookupProperty(this.mChannel.ID, propName);
            if (tpb.Name.ToLower() != propName.ToLower())
            {
                CreateProperty(mChannel.ID, int.Parse(conn.CurrentUser.getInfo("id")), propName, newValue);
            }
            else
            {
                UpdateProperty(mChannel.ID, int.Parse(conn.CurrentUser.getInfo("id")), propName, newValue);
            }
        }
        private static MySqlParameter PValue(string id, object val)
        {
            return new MySqlParameter(id, val);
        }
        private void SendPropertyToClient(IConnection sender, string propName)
        {
            sender.Send(GetPropertyPacket(propName).ToString());
        }
        public PrivclassInfo GetUserPrivclass(IUserData k)
        {
            PrivclassInfo myPrivclass = mDefaultPc;
            foreach (PrivclassInfo pci in pcs)
            {
                if (pci.hasMember(int.Parse(k.getInfo("id"))))
                {
                    myPrivclass = pci;
                    break;
                }
            }
            return myPrivclass;
        }
        public string GetPrivclassName(IConnection conn)
        {
            return GetUserPrivclass(conn.CurrentUser).Name;
        }
        [PacketHandler("join")]
        protected void handlJoinPacket(Packet p, IConnection conn)
        {
            string[] propsToSend = { "title", "topic", "motd", "botdirective" };
            for (int x = 0; x < propsToSend.Length; x++)
                conn.Send(GetPropertyPacket(propsToSend[x]).ToString());
            SendPrivclassPacket(conn);
            SendMembersPacket(conn);
        }
        [PropertyHandler("title", PropertyHandlerMask.GETTER)]
        [PropertyHandler("topic", PropertyHandlerMask.GETTER)]
        [PropertyHandler("motd", PropertyHandlerMask.GETTER)]
        [PropertyHandler("botdirective", PropertyHandlerMask.GETTER)]
        protected void handleGetTextProperties(Packet p, object sender, PropertyHandlerMask mask)
        {
            IConnection conn = (IConnection)sender;
            SendPropertyToClient(conn, p.args["p"]);
        }
        [PropertyHandler("title", PropertyHandlerMask.SETTER)]
        [PropertyHandler("topic", PropertyHandlerMask.SETTER)]
        [PropertyHandler("motd", PropertyHandlerMask.SETTER)]
        [PropertyHandler("botdirective", PropertyHandlerMask.SETTER)]
        protected void handleSetTextProperties(Packet p, object sender, PropertyHandlerMask mask)
        {
            if (!mChannel.Demand((IConnection)sender, new PermissionRule(p.args["p"], 0, Comparison.NEQ)))
            {
                SendErrorPacketToClient((IConnection)sender, p, "not privileged");
                return;
            }
            IConnection conn = (IConnection)sender;
            p.body = SanitizeUserInput(p.body);
            if (p.body == string.Empty)
            {
                SendErrorPacketToClient(conn, p, "nothing to set");
                return;
            }
            SetProperty(conn, p.args["p"], p.body);
            Packet o = GetPropertyPacket(p.args["p"]);
            ((IChannel)mChannel).SendToAll(o.ToString());
        }
        ServerCache<int, PermissionRuleSet> globalPermissions = new ServerCache<int, PermissionRuleSet>(new TimeSpan(0, 0, 20));
        PermissionRuleSet defaultPCPermissions = null;
        DBPropertyManager()
        {
            globalPermissions.CacheMiss = delegate(int id)
            {
                return LoadPermissionSetFromDB(id);
            };
            defaultPCPermissions = globalPermissions[-1];
        }
        private PermissionRuleSet LoadPermissionSetFromDB(int pcid)
        {
            PermissionRuleSet ret = new PermissionRuleSet();
            if (pcid != -1)
            {
                foreach (PermissionRule p in defaultPCPermissions)
                {
                    ret[p.Name] = p;
                }
            }
            using (MySqlConnection conn = SDBackend.CreateConnection())
            {
                using (MySqlDataReader dr = SDBackend.ExecuteReader(conn,
                    "SELECT * FROM tbl_privclass_permissions WHERE privclass_id = @pcid",
                    new MySqlParameter("@pcid", pcid)))
                {
                    while (dr.Read())
                    {
                        PermissionRule tmp = new PermissionRule((string)dr["name"], (int)dr["value"]);
                        ret[tmp.Name] = tmp;
                    }
                }
            }
            return ret;
        }
        public bool Demand(IUserData user, PermissionRule r)
        {
            PrivclassInfo pc = GetUserPrivclass(user);
            PermissionRuleSet p = globalPermissions[pc.Id];
            if (!p.Demand(r))
            {
                if (r.Name != "shownotice")
                    return SDBackend.CurrentInstance.GpcHandler.Demand(new PermissionRule("admin", 0, Comparison.NEQ),
                        user.getInfo("gpc")); //The admin priv for GPC's overrides all permissions.
                else
                    return false;
            }
            return true;
        }
        public void DropPermissionsFromCache(int id)
        {
            globalPermissions.DeleteCacheObject(id);
        }
        public PermissionRuleSet GetPermissions(int id)
        {
            return globalPermissions[id];
        }
        public PermissionRuleSet GetPermissions(PrivclassInfo pc)
        {
            return globalPermissions[pc.Id];
        }
        public bool Demand(IConnection user, PermissionRule r)
        {
            return Demand(user.CurrentUser, r);
        }
        public PrivclassInfo GetPrivclassFromName(string name)
        {
            foreach (PrivclassInfo pc in pcs)
            {
                if (pc.Name == name)
                    return pc;
            }
            return null;
        }
        private PrivclassInfo mDefaultPc = null;
        public Packet getMembersPacket()
        {
            string[] userVars = { "usericon", "symbol", "realname", "typename", "gpc" };
            Packet p = Packet.EmptyPacket;
            Packet Ret = Packet.EmptyPacket;
            foreach (IConnection k in ((IChannel)mChannel).Connections)
            {
                p = Packet.EmptyPacket;
                p.cmd = "member";
                p.param = k.Username;
                //This is going to be the PITA one.
                p.args["pc"] = GetUserPrivclass(k.CurrentUser).Name;
                for (int x = 0; x < userVars.Length; x++)
                {
                    p.args[userVars[x]] = k.CurrentUser.getInfo(userVars[x]);
                }
                p.body = Ret.ToString();
                Ret = p;
            }
            Packet p2 = Packet.EmptyPacket;
            p2.cmd = "property";
            p2.param = this.Namespace;
            p2.args["p"] = "members";
            p2.body = Ret.ToString();
            return p2;
        }
        private void SendMembersPacket(IConnection conn)
        {
            conn.Send(getMembersPacket().ToString());
        }
        //property chat:DataShare
        //p=privclasses
        //
        //99:CoreTeam
        //75:Seniors
        //45:Members
        //42:TestBots
        //40:PoliceBot
        //30:Clients
        //25:Bots
        //10:Silenced
        //1:Banned
        public PrivclassInfo GetGuestPrivclass()
        {
            return mDefaultPc;
        }
        public PrivclassInfo GetNextPrivclass(PrivclassInfo pi)
        {
            WaitForUnlock();
            //Privclasses with the highest order, are stored at index 0
            //Hence why we are doing ++ to find a lower one. and -- to find a higher one
            PrivclassInfo ret = pi;
            int idx = pcs.IndexOf(pi);
            if (idx == 0)
                return null; //Nothing lower. Return error.
            idx--;
            return pcs[idx]; //Next lowest is located at this index.
        }
        public PrivclassInfo GetPreviousPrivclass(PrivclassInfo pi)
        {
            WaitForUnlock();
            //Privclasses with the highest order, are stored at index 0
            //Hence why we are doing ++ to find a lower one. and -- to find a higher one
            PrivclassInfo ret = pi;
            int idx = pcs.IndexOf(pi);
            if (idx == (pcs.Count - 1))
                return null; //Nothing higher. Return error.
            idx++;
            return pcs[idx]; //Next highest is located at this index.
        }
        public PrivclassInfo FindBannedPrivclass()
        {
            for (int x = pcs.Count - 1; x >= 0; x--)
            {
                if (globalPermissions.GetCacheObject(pcs[x].Id).Demand(new PermissionRule("join", 0, Comparison.EQ)))
                    return pcs[x];
            }
            return null;
        }
        public PrivclassInfo[] GetAllPrivclasses()
        {
            return pcs.ToArray();
        }
        const string sqlDeletePCMemberRecord = "DELETE FROM `tbl_privclass_members` WHERE `id`=@oldpc AND `memberid`=@memberid";
        const string sqlUpdatePCMemberRecord = "UPDATE `tbl_privclass_members` SET `id`=@newpc WHERE `id`=@oldpc AND `memberid`=@memberid";
        const string sqlCreatePCMemberRecord = "INSERT INTO `tbl_privclass_members` (`id`, `memberid`) VALUES (@newpc, @memberid)";
        public bool SetUserPrivclass(IUserData user, PrivclassInfo target)
        {
            int PrivclassID = target.Id;
            MySqlParameter userid = new MySqlParameter("@memberid", null);
            userid.Value = int.Parse(user.getInfo("id"));
            MySqlParameter oldpc = new MySqlParameter("@oldpc", null);
            oldpc.Value = GetUserPrivclass(user).Id;
            MySqlParameter newpc = new MySqlParameter("@newpc", null);
            newpc.Value = PrivclassID;
            if (PrivclassID == mDefaultPc.Id)
            {
                //Delete the record.
                SDBackend.ExecuteNonQuery(sqlDeletePCMemberRecord, oldpc, userid);
            }
            else
            {
                if (GetUserPrivclass(user).Id == PrivclassID)
                {
                    return false; //Do NOT update the database.
                }
                if (SDBackend.ExecuteNonQuery(sqlUpdatePCMemberRecord, newpc, oldpc, userid) == 0)
                {
                    SDBackend.ExecuteNonQuery(sqlCreatePCMemberRecord, newpc, userid);
                }
            }
            //Since we updated the database, Reload all privclasses, Its not perfect, But it works.
            LoadPCs();
            return true;
        }
        ManualResetEvent pcLock = new ManualResetEvent(true);
        protected void LockPCs()
        {
            lock (pcLock)
                pcLock.Reset();
        }
        protected void UnlockPCs()
        {
            lock (pcLock)
                pcLock.Set();
        }
        public PrivclassInfo[] GetPrivclasses()
        {
            return pcs.ToArray();
        }
        public Packet GetPrivclassPacket()
        {
            Packet outgoing = Packet.EmptyPacket;
            outgoing.cmd = "property";
            outgoing.param = this.Namespace;
            outgoing.args["p"] = "privclasses";
            StringBuilder sb = new StringBuilder();
            for (int x = 0; x < pcs.Count; x++)
            {
                sb.AppendFormat("{0}:{1}\n", pcs[x].Order, pcs[x].Name);
            }
            outgoing.body = sb.ToString();
            return outgoing;
        }
        public void SendPrivclassPacket(IConnection conn)
        {
            //This wait for unlock is not really needed anywhere. We should remove all of these and the function for it.

            WaitForUnlock();
            conn.Send(GetPrivclassPacket().ToString());
        }
        List<PrivclassInfo> pcs = new List<PrivclassInfo>();
        protected void WaitForUnlock()
        {
            pcLock.WaitOne(-1);
        }
        private void LoadPCs()
        {
            ReLoadPCs();
        }
        public void ReLoadPCs()
        {
            try
            {
                //Let other threads get past the lock.
                Thread.Sleep(0);
                WaitForUnlock(); //Wait if its already locked.
                LockPCs(); //And finally lock it.
                //Let processing threads finish.
                Thread.Sleep(100);
                //We should be safe here.
                pcs.Clear();
                using (MySqlConnection conn = SDBackend.CreateConnection())
                {
                    using (MySqlDataReader reader = SDBackend.ExecuteReader(conn,
                        "SELECT * FROM tbl_privclass WHERE `channel` = @channel",
                        new MySqlParameter("@channel", this.mChannel.ID)))
                    {
                        while (reader.Read())
                        {
                            PrivclassInfo tmp = new PrivclassInfo(reader);
                            pcs.Add(tmp);
                            if (tmp.Order == mChannel.defaultPC)
                                this.mDefaultPc = tmp;
                        }
                    }
                }
                pcs.Sort(delegate(PrivclassInfo a, PrivclassInfo b)
                {
                    return b.Order.CompareTo(a.Order);
                });
            }
            finally
            {
                //Make damn sure the lock is ALWAYS released.
                UnlockPCs();
            }
        }
        [PropertyHandler("privclasses", PropertyHandlerMask.SETTER)]
        [PropertyHandler("privclasses", PropertyHandlerMask.GETTER)]
        [PropertyHandler("members", PropertyHandlerMask.GETTER)]
        [PropertyHandler("members", PropertyHandlerMask.SETTER)]
        protected void handleGetPrivclasses(Packet p, object sender, PropertyHandlerMask mask)
        {
            if (mask == PropertyHandlerMask.SETTER)
            {
                SendErrorPacketToClient((IConnection)sender, p, "read-only property");
                return;
            }
            switch (p.args["p"].ToLower())
            {
                case "privclasses":
                    SendPrivclassPacket((IConnection)sender);
                    break;
                case "members":
                    SendMembersPacket((IConnection)sender);
                    break;
            }
        }
    }
}
